package model;

import java.util.ArrayList;

/**
 * This class represents our game board.
 * 
 * Notice that any method marked deprecated is not part of the official interface.
 */

public class OthelloBoard
{
	/*!!! be sure to copy these variables in reset() and/or the copy constructor*/
	public int size = 8; 				// size should be even
	private byte[][] board;				// internal board representation
	private int time;
	public Position last_move = null;
	boolean symmetry_removal = true;	// We do not remove the symmetries if symmetry_removal is false.
	int input_representation = 1;		// Determines if we expect 1, 2 or 3 inputs per square
	/*!!! be sure to copy these variables in reset() and the copy constructor*/
	
	public OthelloBoard()
	{
		board = new byte[size][size];
		reset();
	}
	
	/**
	 * @param other  -- allows you to copy other board.
	 */
	public OthelloBoard(OthelloBoard other)
	{//copy constructor
		time = other.time;
		size = other.size;
		board = new byte[size][size];
		last_move = other.last_move;
		input_representation = other.input_representation;
		symmetry_removal = other.symmetry_removal;
		
		for(int i = 0; i < size; i++)
			for(int j = 0; j < size; j++)
				this.board[i][j] = other.board[i][j];
	}
	
	public void setBoard(OthelloBoard other) 
	{
		time = other.time;
		size = other.size;
		board = new byte[size][size];
		last_move = other.last_move;
		
		for(int i = 0; i < size; i++)
			for(int j = 0; j < size; j++)
				this.board[i][j] = other.board[i][j];
	}
	
	/**
	 * bounds check on i and j
	 * @param i - x coord to be checked
	 * @param j - y coord to be checked
	 * @return  true if in bounds, false if out.
	 */
	private boolean inBounds(int i, int j)
	{
		return i >= 0 && i < size &&
		       j >= 0 && j < size;
	}
	
	/**
	 * This method determines if any position is the start of a sequence (basically if it's a valid move).
	 * @param pos  - Position to check
	 * @param color - Color of the player who wants to make a move there.
	 * @return  -  true if it's a valid move, false if not.
	 */
	private boolean isSequenceStart(Position pos, byte color)
	{ // move from square (i,j) over opponent to color
		
		//a priori impossibilities
		if(!inBounds(pos.x, pos.y) || board[pos.x][pos.y] != Field.EMPTY)
			return false;

		//possible enclosed sequences
		if(isSequence(pos, -1,  0, color)){
			return true; 
		} // north
		if(isSequence(pos, -1,  1, color)) {
			return true; 
		} //north east
		if(isSequence(pos,  0,  1, color)){
			return true; 
		} //east
		if(isSequence(pos,  1,  1, color)) {
			return true; 
		} //south east
		if(isSequence(pos,  1,  0, color)) {
			return true; 
		} //south
		if(isSequence(pos,  1, -1, color)) {
			return true; 
		} //south west
		if(isSequence(pos,  0, -1, color)) {
			return true; 
		} //west
		if(isSequence(pos, -1, -1, color)) {
			return true; 
		} //north west
		
		//no possibilities
		return false;
	}
	
	
	/**
	 * Method used to determine if there is a sequence in direction di/dj from pos, for color. 
	 * @param pos - position from which to start checking
	 * @param di - direction X to check
	 * @param dj - direction Y to check
	 * @param color - color of the player who wants to use this sequence
	 * @return  - true if a sequence was found, false if not.
	 */
	private boolean isSequence(Position pos, int di, int dj, byte color)
	{
		byte opponent = Field.opponent(color);
		
		//callCount++; used to measure calls in performance runs.
		// 1 liner loop to determine k.
		int k = 1;
		for(;inBounds(pos.x + di*k, pos.y + dj*k) && board[pos.x + di*k][pos.y + dj*k] == opponent; k++);
		
		if(inBounds(pos.x + di*k, pos.y + dj*k) && board[pos.x + di*k][pos.y + dj*k] == color && k > 1) 
		{
			return true;
		}
		return false;
	}
	
	/**
	 * Method to be used to flip all fields from pos in direction of di  and dj  
	 * @param pos - Starting position
	 * @param di - Direction x
	 * @param dj - Direction y
	 * @param color - Color of the player flipping the fields
	 */
	private void flipSequence(Position pos, int di, int dj, byte color)
	{//only use when sure of enclosed sequence
 		byte opponent = Field.opponent(color);
		int k = 1;
		while(board[pos.x + di*k][pos.y + dj*k] == opponent) 
		{
			board[pos.x + di*k][pos.y + dj*k] = color;
			k++;
		}
	}
	
	/**
	 * Method used to obtain an arraylist of possible transitions.
	 * @param color - color of the player who wants to know possible transitions
	 * @return - the result
	 */
	public ArrayList<Position> getTransitions(byte color)
	{	
		ArrayList<Position> positions = new ArrayList<Position>();
		for(short i = 0; i < size; i++) 
		{
			for(short j = 0; j < size; j++) 
			{
				Position pos = new Position(i, j);
				if(isSequenceStart(pos, color)) 
				{
					positions.add(pos);
				}
			}
		}
		return positions;
	}
	
	/**
	 * This methods causes the player with the supplied color to take the field at position pos.
	 * @param pos - the position to take
	 * @param color - the color to give to the position (and to any sequences made from there).
	 */
	public void makeTransition(Position pos, byte color)
	{//only use when sure of legal move
		time++;
		board[pos.x][pos.y] = color;
		last_move = pos;
		
		if(isSequence(pos, -1,  0, color)) {
			flipSequence(pos, -1, 0, color); 	// north 
		}
		if(isSequence(pos, -1,  1, color)) {
			flipSequence(pos, -1, 1, color); 	// north east
		}
		if(isSequence(pos,  0,  1, color)) {
			flipSequence(pos, 0, 1, color);	// east
		}
		if(isSequence(pos,  1,  1, color)) {
			flipSequence(pos, 1, 1, color); 	// south east
		}
		if(isSequence(pos,  1,  0, color)) {
			flipSequence(pos, 1, 0, color); 	// south
		}
		if(isSequence(pos,  1, -1, color)) {
			flipSequence(pos, 1, -1, color);	// south west
		}
		if(isSequence(pos,  0, -1, color)) {
			flipSequence(pos, 0, -1, color);	// west
		}
		if(isSequence(pos, -1, -1, color)) {
			flipSequence(pos, -1, -1, color); 	// north west
		}  
	}
	
	public boolean isFinalState()
	{
		return getTransitions(Field.WHITE).size() == 0 && 
			   getTransitions(Field.BLACK).size() == 0;
	}
	
	/**
	 * @return - the color of the player with the most fields of his/her color at this time.
	 */
	public byte bestPlayer()
	{
		int[] nrSquares = new int[3]; //empty, white, black
		for(int i = 0; i < size; i++)
			for(int j = 0; j < size; j++)
				nrSquares[board[i][j]]++;
		return nrSquares[Field.WHITE] > nrSquares[Field.BLACK] ? Field.WHITE : 
			   nrSquares[Field.WHITE] < nrSquares[Field.BLACK] ? Field.BLACK : Field.EMPTY;
	}
	
	/**
	 * The method can be used to obtain the value of a certain field
	 * @param i the i position of the field
	 * @param j the j position of the field
	 * @return  the value of the field
	 */
	public byte at(int i, int j)
	{
		return board[i][j];
	}
	
	/**
	 * This method puts the current board (ascii style) into a String and returns that.
	 */
	public String toString()
	{		
		String str = " ";
		for(int i = 0; i < size; i++)
			str += " " + i;
		str += "\n";
		for(int i = 0; i < size; i++)
		{
			str += i + "|";
			for(int j = 0; j < size; j++)
				str += Field.toChar(board[i][j]) + "|";
			str += "\n";
		}
		return str;
	}
	
	/**
	 * Method used to determine the amount of points any given player has.
	 * @param color - the color of the player to get the count for.
	 * @return - the score.
	 */
	public int getScore(byte color) {
		int[] nrSquares = new int[3]; //empty, white, black
		for(int i = 0; i < size; i++)
			for(int j = 0; j < size; j++)
				nrSquares[board[i][j]]++;
		return nrSquares[color];
	}
	
	public int getTime()
	{
		return time;
	}
	
	/**
	 * Make a clone of this game board (for simulation purposes).
	 * @return - An exact copy of the current board.
	 */
	public OthelloBoard getClone() 
	{
		return new OthelloBoard(this);
	}

	/**
	 * Method used by Neural Network to get a double representation of the gameboard
	 * @return the representation
	 */
    public double[] getDoubleArrayRepresentation()
    {
        double rep[] = new double[size*size];
        for (int i = 0; i < size; i++) 
        {
        	for (int j = 0; j < size; j++) 
        	{
        		rep[i*size+j] = board[i][j];
        	}
        }
        return rep;
    }
    
    private byte[] removeSymmetry()
    {
    	//generate the 8 symmetries and return the 'smallest'
    	//    1|  2|
    	// 0-> 1 2 3 <-3
    	//     4 5 6 
    	// 7-> 7 8 9 <-4
    	//    6|  5|
    	byte[][] symmetries = new byte[8][64];
    	//check which symmetry to keep
    	boolean[] inTheRunning = {true, true, true, true, true, true, true, true};
    	for(int i = 0; i < size; i++)
        	for(int j = 0; j < size; j++)
        	{
        		symmetries[0][i*size + j] = board[i][j];
        		symmetries[1][i*size + j] = board[j][i];
        		symmetries[2][i*size + j] = board[size-1 - i][j];
        		symmetries[3][i*size + j] = board[size-1 - j][i];
        		symmetries[4][i*size + j] = board[i][size-1 - j];
        		symmetries[5][i*size + j] = board[j][size-1 - i];
        		symmetries[6][i*size + j] = board[size-1 - i][size-1 - j];
        		symmetries[7][i*size + j] = board[size-1 - j][size-1 - i];
        		//find the smallest for this position
        		byte smallest = 2;
        		for(int k = 0; k < 8; k++)
        			if(inTheRunning[k] && symmetries[k][i*size + j] < smallest)
        				smallest = symmetries[k][i*size + j];
        		//cross off all with a larger value
        		for(int k = 0; k < 8; k++)
        			if(inTheRunning[k] && symmetries[k][i*size + j] > smallest)
        				inTheRunning[k] = false;
        	}
    	for(int k = 0; k < 8; k++)
    		if(inTheRunning[k])
    			return symmetries[k];
    	System.out.println("ERROR: something went wrong in OthelloBoard.removeSymmetry()...");
    	return null;
    }
    
    public double[] toArray(byte color)
    {
    	byte[] ret_bytes = new byte[size*size]; //byte vector representation, without use of color
    	if (symmetry_removal) 
    	{ //map board to 1 of the 8 byte vectors in symmetry group 
	    	ret_bytes = removeSymmetry(); 
    	}
    	else 
    	{ //map board directly to byte vector
        	ret_bytes = getByteArrayRepresentation();
    	}
    	// map byte vector to double vector, using color and representation
    	if (input_representation == 1) 
    	{
    		double[] represented_ret = new double[size*size*input_representation];
    		for(int i = 0; i < ret_bytes.length; i++)
    			represented_ret[input_representation * i + 0] = ret_bytes[i] == color ?  1.0 : ret_bytes[i] == Field.opponent(color) ? -1.0 :  0.0;
    		//System.out.println("length = " + represented_ret.length);
    		return represented_ret;
    	}
    	else if (input_representation == 2) 
    	{
    		double[] represented_ret = new double[size*size*input_representation];
    		for(int i = 0; i < ret_bytes.length; i++)
    		{
    			represented_ret[input_representation * i + 0] = ret_bytes[i] == color ?  1.0 : ret_bytes[i] == Field.opponent(color) ? -1.0 : 0.0;
    			represented_ret[input_representation * i + 1] = ret_bytes[i] == color ? -1.0 : ret_bytes[i] == Field.opponent(color) ?  1.0 : 0.0;
    		}
    		//System.out.println("length = " + represented_ret.length);
    		return represented_ret;
    	}
    	else if (input_representation == 3) 
    	{
    		double[] represented_ret = new double[size*size*input_representation];
    		for(int i = 0; i < ret_bytes.length; i++)
    		{
    			represented_ret[input_representation * i + 0] = ret_bytes[i] == color ?  1.0 : ret_bytes[i] == Field.opponent(color) ? -1.0 : 0.0;
    			represented_ret[input_representation * i + 1] = ret_bytes[i] == color ? -1.0 : ret_bytes[i] == Field.opponent(color) ?  1.0 : 0.0;
    			represented_ret[input_representation * i + 2] = ret_bytes[i] == color ? -1.0 : ret_bytes[i] == Field.opponent(color) ?  1.0 : 0.0;
    		}
    		//System.out.println("length = " + represented_ret.length);
    		return represented_ret;
    	}
    	System.out.println("ERROR: something went wrong in OthelloBoard.toArray(byte)...");
    	return null; // this gets returned if the representation is unknown (not 1 2 or 3 (64, 128, 192))
    }
    
    /**
     * Method used to obtain a 1D byte representation of the board.
     * @return the represenation
     */
    public byte[] getByteArrayRepresentation()
    {
        byte rep[] = new byte[size*size];
        for (int i = 0; i < size; i++) 
        {
        	for (int j = 0; j < size; j++) 
        	{
        		rep[i*size+j] = board[i][j];
        	}
        }
        return rep;
    }

	public double getReward(byte color) 
	{
		double own = getScore(color);
		double other = getScore(Field.opponent(color)); // needs to be double, or the next line rounds.
		return 2/(1+Math.exp(-5*(own - other)/(own + other)))-1;
		//return 2/(1+Math.exp(-5*(own - other)/(own + other)))-1; 
		/*if(bestPlayer() == color) 
			return 1.0;
		if(bestPlayer() == Field.opponent(color)) 
			return -1.0;
		return 0.0;*/
	}

	public void reset() 
	{//do not reset game-settings, only board and time
		time = 0;
		
		for(int i = 0; i < size; i++)
			for(int j = 0; j < size; j++)
				board[i][j] = Field.EMPTY;
	
		board[size/2 - 1][size/2 - 1] = board[size/2][size/2] = Field.WHITE;
		board[size/2 - 1][size/2] = board[size/2][size/2 - 1] = Field.BLACK;
	}
	
	public void setSymmetryRemoval(boolean symmetryRemoval) 
	{
		symmetry_removal = symmetryRemoval; 
	}
	
	public void setInputRepresentation(int inputRepresentation) 
	{
		input_representation = inputRepresentation;
		if(input_representation < 1 || input_representation > 3)
			System.out.println("ERROR: unknown inputRepresenation: " + inputRepresentation);
	}
	
	public static void main(String[] arg)
	{
		//create some random board
		OthelloBoard board = new OthelloBoard();
		new players.RandomPlayer().move(board, Field.WHITE);
		new players.RandomPlayer().move(board, Field.BLACK);
		new players.RandomPlayer().move(board, Field.WHITE);
		new players.RandomPlayer().move(board, Field.BLACK);
		new players.RandomPlayer().move(board, Field.WHITE);
		new players.RandomPlayer().move(board, Field.BLACK);
		

		board.setInputRepresentation(1);
		double[] een = board.toArray(Field.WHITE);		
		System.out.println("\nrestored board 3:");
		for(int i = 0; i < 64; i++)
			System.out.print(een[i] + ", ");
		System.out.println();
		
		board.setInputRepresentation(2);
		double[] twee = board.toArray(Field.WHITE);		
		System.out.println("\nrestored board 3:");
		for(int i = 0; i < 64; i++)
			System.out.print(twee[2*i + 0] + " " + twee[2*i+1]+ ", ");
		System.out.println();
		
		board.setInputRepresentation(3);
		double[] drie = board.toArray(Field.WHITE);		
		System.out.println("\nrestored board 3:");
		for(int i = 0; i < 64; i++)
			System.out.print(drie[3*i + 0] + " " + drie[3*i+1] + " " + drie[3*i+2] + ", ");
		System.out.println();
		//QED
	}
}
